# This file is intended to be a final submission. python tester.py Player.py
# should work at all times. If it does not, there is a bug.
# If you're just trying to test a solution, scroll down to the Player
# class.

# This file is intended to be in the same format as a valid solution, so
# that users can edit their solution into Player and then submit just this
# file to the contest. If you see any reason this would not work, please submit
# an Issue to https://github.com/ChadAMiller/hungergames/issues or email me.

# You can see more sample player classes in bots.py

class BasePlayer(object):
    '''
    Base class so I don't have to repeat bookkeeping stuff.
    Do not edit unless you're working on the simulation.
    '''
    
    def __str__(self):
        try:
            return self.name
        except AttributeError:
            # Fall back on Python default
            return super(BasePlayer, self).__repr__()
    
    def hunt_choices(*args, **kwargs):
        raise NotImplementedError("You must define a strategy!")
        
    def hunt_outcomes(*args, **kwargs):
        pass
        
    def round_end(*args, **kwargs):
        pass


from collections import Counter

class Player(BasePlayer):
    '''
    Your strategy starts here.
    '''

    def __init__(self, *args, **kw):
        BasePlayer.__init__(self, *args, **kw)

        self.p_hunt = 0.5

        self.past_sizes = []
        self.past_reps = []

        self.past_ids = []
        self.past_coops = []
        self.past_outcomes = []

    def move_if_mapped(self, i, j, round_number, player_reputations):
        # let's just work out what they played last round
        last_reps = self.past_reps[-1]
        rep_now = player_reputations[i]
        rep_prev = last_reps[j]
        coops_now = rep_now * sum(self.past_sizes)
        coops_prev = rep_prev * sum(self.past_sizes[:-1])
        return coops_now - coops_prev

    def istitfortatbot(self, ids):
        mymoves = [self.past_outcomes[t][j][0] for (t,j) in enumerate(ids)]
        itmoves = [self.past_outcomes[t][j][1] for (t,j) in enumerate(ids)][1:]
        counts = Counter(zip(mymoves, itmoves))
        if counts:
            CD = counts.get(('C', 'D'), 0)
            CC = counts.get(('C', 'C'), 0)
            DD = counts.get(('D', 'D'), 0)
            DC = counts.get(('D', 'C'), 0)
            # I should probably cooperate with them if playing C increases the chances of getting C significantly (by more than 1/3)
            pc = (CC + 1.0) / (CC + CD + 2.0)
            pd = (DC + 1.0) / (DC + DD + 2.0)
            # but we'll fudge it to 2/3 since these should be a lot less common
            return pc - pd > 2.0/3.0
        return False

    def hunt_choices(self, round_number, current_food, current_reputation, m, player_reputations):
        n = len(player_reputations)

        if round_number > 1:
            last_reps = self.past_reps[-1]
            last_n = self.past_sizes[-1]

            # a stupid algorithm for derandomizing
            mapped = {}
            for _ in range(n):
                # match the player with worst best matching first
                best = None
                for i in range(n):
                    if i in mapped.keys():
                        continue
                    available = [j for j in range(last_n) if j not in mapped.values() and self.move_if_mapped(i, j, round_number, player_reputations) >= 0]
                    if not available:
                        # we've buggered up somewhere, but it would be too expensive to go back at fix it
                        # just take whatever, even if it's impossible
                        available = [j for j in range(last_n) if j not in mapped.values()]
                    (error, target) = min((abs(last_reps[j] - player_reputations[i]), j) for j in available)
                    if best is None or (error, target, i) < best:
                        best = (error, target, i)
                (_, j, i) = best
                mapped[i] = j
            self.past_ids = [self.past_ids[mapped[i]] + [mapped[i]] for i in range(n)]

            players = []
            for i in range(n):
                # let's work out what they played in each round
                ids = self.past_ids[i]
                reps = [self.past_reps[t][ids[t]] for t in range(round_number - 1)] + [player_reputations[i]]
                coops = [reps[t] * sum(self.past_sizes[:t]) for t in range(round_number)]
                moves = [int(0.5 + coops[t+1] - coops[t]) for t in range(round_number-1)]

                # the move list isn't guaranteed to be nonnegative, since our derandomizer is approximate
                # so we'll just have to live with that

                # figure out what strategy everyone is playing
                if player_reputations[i] == 0.0:
                    # they're most likely a defectbot
                    players.append('defect')
                elif player_reputations[i] == 1.0:
                    # probably a cooperatebot, even though that's a silly strategy
                    players.append('coop')
                elif self.istitfortatbot(ids):
                    # a derandomizing tit-for-tat bot similar to myself
                    players.append('tft')
                else:
                    pcoops = [float(moves[t] + 1.0) / float(self.past_sizes[t] + 2.0) for t in range(len(moves))]
                    avg_reps = [sum(self.past_reps[t]) / self.past_sizes[t] for t in range(len(moves))]
                    # fairbot's move in each round should be close to the average for the previous round
                    fair_deviation = sum(abs(a - b)**2 for (a, b) in zip(pcoops, avg_reps))
                    # randombot's move should be close to its current reputation
                    random_deviation = sum(abs(a - player_reputations[i])**2 for a in pcoops)
                    # print fair_deviation, random_deviation
                    if fair_deviation <= random_deviation:
                        players.append('fair')
                    else:
                        players.append('random')
            # print players
        else:
            # first round, do whatever
            self.past_ids = [[] for _ in range(n)]

        self.past_sizes.append(n)
        self.past_reps.append(player_reputations)

        if round_number > 1:
            # now start deciding what to do
            if len(player_reputations) == 1:
                # almost everyone's dead, it's a war of attrition
                return ['s'] * n

            player_counts = Counter(players)
            # we'll decide what to do based on what kind of players are here
            fairbots = player_counts.get('fair', 0)
            # I gain a certain amount of reputation by hunting
            # depending on the number of fairbots around, it might
            # be worth the cost now to pay off later.
            # Unfortunately there's no rigorous way to decide how far I should look for 'later'?
            print 'current food:', current_food, '/n', current_food/n
            MAGIC_TIME_HORIZON = current_food/n # extremely rough guess of how long I can expect to live right now
            num_hunts = sum(self.past_sizes[:-1])
            # it's a harmonic series, which diverges, which is why I can't just use an infinite time horizon
            marginal_rep = sum(1.0 / float(num_hunts + n + k*n) for k in range(MAGIC_TIME_HORIZON))
            marginal_gain = marginal_rep * fairbots
            if marginal_gain > 1.0:
                # if I gain anything by doing this, cooperate with everyone
                return ['h'] * n
            else:
                # we should defect against everyone except for the tftbots
                strategy = ['s'] * n
                for i in range(n):
                    if players[i] == 'tft':
                        strategy[i] = 'h'
                return strategy
        else:
            # in the first round, cooperate with everyone to get reputation
            return ['h'] * n        

    def hunt_outcomes(self, food_earnings):
        '''Required function defined in the rules'''
        TRANSLATE = {0 : 'CC', 1 : 'DC', -2 : 'DD', -3 : 'CD'}
        self.past_outcomes.append([TRANSLATE[food] for food in food_earnings])

    def round_end(self, award, m, number_hunters):
        '''Required function defined in the rules'''
        pass
        
